import Dep from "./dep"

let id = 0

// 观察者模式--即每个被代理的属性都有一个dep（依赖），每次属性变化都会触发对应的watcher更新

class Watcher {
  constructor(vm, exOrFn, options, cb) {
    this.deps = []
    this.id = id++

    if (typeof exOrFn === "string") {
      // 这里代表了是用户写的watch,传入的可能是字符串,此时没有lazy,所以一进来就会调用getter取值，
      // 这个watcher入watchers栈，并且Dep.targe指向此watcher
      // 在取值vm[exOrFn]的时候会走到data代理的get上，此时进行依赖收集，就会收集到这个watcher，dep内存的这个watcher的顺序在最前面，
      // 因为此后才会进行挂载，进入渲染watcher在进入到render函数取值到计算属性进入到计算属性watcher的依赖收集
      this.getter = function () {
        // 处理监听对象的内部的属性如："jj.fqm"
        let result = vm
        let keys = exOrFn.split('.')
        keys.forEach(key => {
          result = result[key]
        })
        return result
      }
    } else {
      this.getter = exOrFn
    }

    this.vm = vm
    this.renderWatcher = options.renderWatcher
    this.cb = cb
    // 去重
    this.depIds = new Set()
    // 如果传入了lazy，说明是计算属性Wacther，并且一开始计算属性Watcher的dirty一开始就是true
    this.dirty = this.lazy = options.lazy

    if (!this.lazy) {
      this.get()
    }
  }
  addDep(dep) {
    // 可能在template内部使用多次同一属性
    if (!this.depIds.has(dep.id)) {
      this.deps.push(dep)
      this.depIds.add(dep.id)
      dep.addSub(this)  // 此时watcher已经记录了所有dep并且去重，此时再让所有的dep记录watcher，也是去重的
    }
  }
  calculate() {
    this.value = this.get()
    this.dirty = false
  }
  depend() {
    this.deps.forEach(dep => {
      dep.depend()
    })
  }
  get() {
    pushWacther(this)
    // 对于计算属性watcher，这个getter函数就是计算属性的getter函数
    // 对于renderWatcher，这个getter函数就是update函数，渲染dom
    // 对于watch，这个getter函数就是watch监听值的getter函数
    let value = this.getter.call(this.vm)
    popWacther()
    return value
  }
  update() {
    if (this.cb) {
      this.cb()
      return
    }

    // 计算属性watcher的lazy为true
    if (this.lazy) {
      // 计算属性第一次渲染时不会自动触发其get函数，只有当计算属性触发其get函数时，基于dirty判断是否需要重新取值
      // 计算属性watcher
      this.dirty = true
    } else {
      // renderWatcher
      joinQueue(this)
    }
  }
}

// 存放所有watcher
let watchers = []
// 注意，Watcher的存储虽然是逻辑关系，但是其实际上有隐形的先后顺序，自定义watcher > 渲染watcher > 计算属性watcher（监听器watcher是第一被dep收集的watcher，此后收集计算属性的，最后才是渲染watcher的）
// 因为计算属性的watcher是在render函数里get触发才会调用watcher.get把当前的watcher推到watchers的栈中
// 那么什么时候调用watcher？在渲染watcher进行更新的时候调用，此时已经走到了渲染watcher的get方法，也就是传入的_update(_render())
// 此时再在watcher.get内调用_render，所以在计算属性触发其get前，已经存在了渲染watcher，那么在计算属性的get内
// 第一次渲染一定会走到对应的data内数据的get,此时Dep的target就是计算属性的watcher,此时进行收集watcher,计算属性的watcher就会被收集，watcher也会拿到所有的依赖dep
// 此后，取值完成，watchers的栈会弹出计算属性watcher（栈中还剩渲染watcher），并将渲染watcher指向Dep.target，
// 此时对计算属性的watcher的所有dep进行遍历，全部进行依赖收集，收集Dep.target，也就是计算属性get内每次收集栈中的上一个watcher
// 如果又多个计算属性，每次走完get之后都会弹出watchers栈，再遍历计算属性依赖的所有dep，收集渲染watcher，此时会重复收集，之前写的set去重就生效了
// 这里的计算属性的get指的是defineProperty定义的get方法

function pushWacther(watcher) {
  Dep.target = watcher
  watchers.push(watcher)
  // console.log(watcher)
}

function popWacther() {
  watchers.pop()
  Dep.target = watchers[watchers.length - 1]
}

// 异步按批次更新(渲染wacther)
let queue = []
let ids = {}
let pedding = false

function joinQueue(watcher) {
  if (!ids[watcher.id]) {
    ids[watcher.id] = true
    queue.push(watcher)
    flushQueue()
    pedding = true
  }
}

// 这里通过异步同步的先后逻辑实现在同步后，同意调用nextTick里的回调
// 同一时刻多次触发watcher的更新时，会多次调用joinQueue，但只会立即走完同步，还会往queue队列里塞数据，仅第一个会触发flushQueue的if逻辑，此时pedding为true，
// 其他的会在下一个tick里，也就是同步执行完进行异步时，执行flushQueue里的nextTick，取之前填入队列的watcher，调用get方法，这样就实现了一轮次为单位的异步更新
function flushQueue() {
  if (!pedding) {
    nextTick(() => {
      // 复制数组，如此在进行异步任务的时候仍然可以往queue里塞watcher
      let copy = queue.slice(0)
      console.log('queue', queue)
      queue = []
      ids = {}
      // get就是渲染组件的update(render)方法或者是计算属性的getter
      copy.forEach(q => q.get())
      pedding = false
    })
  }
}

let cbs = []
let cbing = false
export function nextTick(cb) {
  cbs.push(cb)
  if (!cbing) {
    TimerFunc(() => {
      let copy = cbs.slice(0)
      cbs = []
      copy.forEach(cb => cb())
      cbing = false
    })
  }
  cbing = true
}

function TimerFunc(cb) {
  // 模拟dom更新繁忙，队列阻塞
  // setTimeout(cb, 1000)

  if (Promise) {
    Promise.resolve().then(cb)
  } else if (setTimeout) {
    setTimeout(cb)
  }
}

export default Watcher